﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Text;
using LinFu.IoC.Configuration;
using LinFu.Proxy.Interfaces;
using LinFu.Reflection.Emit;
using Mono.Cecil;
using Mono.Cecil.Cil;
using LinFu.IoC.Interfaces;
using LinFu.Reflection;
using CoreEx.Common.Extensions;
using EventAttributes=Mono.Cecil.EventAttributes;
using FieldAttributes=Mono.Cecil.FieldAttributes;
using MethodAttributes=Mono.Cecil.MethodAttributes;

namespace CoreEx.Common.Implementation.Ioc
{
    [Implements(typeof(IMethodBodyEmitter),ServiceName = "INotifyPropertyChanged")]
    public class NotifyPropertyChangedEmitter : IMethodBodyEmitter, IInitialize<IServiceRequestResult>
    {
        private TypeDefinition _baseType;
        private FieldDefinition _eventHander;
        private MethodDefinition _onPropertyChangedMethod;


        public NotifyPropertyChangedEmitter(TypeDefinition typeDefinition)
        {
            _baseType = typeDefinition;
        }

        public void Emit(MethodInfo originalMethod, MethodDefinition targetMethod)
        {
            if (originalMethod.Name == "add_PropertyChanged")
                EmitAddHandler(targetMethod);
            else
                EmitRemoveHandler(targetMethod);
        }

        protected virtual void EmitAddHandler(MethodDefinition targetMethod)
        {         
            var combineMethod = targetMethod.DeclaringType.Module.ImportMethod<Delegate>("Combine",new []{typeof(Delegate),typeof(Delegate)});             
            CilWorker cilWorker = targetMethod.GetILGenerator();
            cilWorker.Emit(OpCodes.Ldarg_0);
            cilWorker.Emit(OpCodes.Dup);
            cilWorker.Emit(OpCodes.Ldfld, _eventHander);
            cilWorker.Emit(OpCodes.Ldarg_1);
            cilWorker.Emit(OpCodes.Call, combineMethod);
            cilWorker.Emit(OpCodes.Castclass, _eventHander.FieldType);
            cilWorker.Emit(OpCodes.Stfld, _eventHander);
            cilWorker.Emit(OpCodes.Ret);
        }

        protected virtual void EmitRemoveHandler(MethodDefinition targetMethod)
        {
            var removeMethod = targetMethod.DeclaringType.Module
                .ImportMethod<Delegate>("Remove", new [] { typeof(Delegate), typeof(Delegate) });             
            CilWorker cilWorker = targetMethod.GetILGenerator();
            cilWorker.Emit(OpCodes.Ldarg_0);
            cilWorker.Emit(OpCodes.Dup);
            cilWorker.Emit(OpCodes.Ldfld, _eventHander);
            cilWorker.Emit(OpCodes.Ldarg_1);
            cilWorker.Emit(OpCodes.Call, removeMethod);
            cilWorker.Emit(OpCodes.Castclass, _eventHander.FieldType);
            cilWorker.Emit(OpCodes.Stfld, _eventHander);
            cilWorker.Emit(OpCodes.Ret);
        }

        
        /// <summary>
        /// Emits the OnPropertyChanged method that is called by every property setter
        /// </summary>
        /// <remarks>
        /// The code generated at runtime is equalent to the code snippet below. 
        /// </remarks>        
        /// <code>
        ///private void OnPropertyChanged(string propertyName)
        ///{
        ///    if (_propertyChangedEventHandler != null)            
        ///        _propertyChangedEventHandler(this, new PropertyChangedEventArgs(propertyName));            
        ///}
        /// </code>
        protected virtual void EmitOnPropertyChanged()
        {

            var constructor = _baseType.Module.ImportConstructor<PropertyChangedEventArgs>(new [] {typeof (string)});

            var invokeMethod = _baseType.Module.ImportMethod<PropertyChangedEventHandler>("Invoke");

            
            _onPropertyChangedMethod = _baseType.DefineMethod("OnPropertyChanged", MethodAttributes.Private | MethodAttributes.HideBySig, typeof(void),
                                                   new [] {typeof (string)},new Type[]{});
            _onPropertyChangedMethod.Body.InitLocals = true;                        
            
            
            var cilWorker = _onPropertyChangedMethod.GetILGenerator();
            
            _onPropertyChangedMethod.AddLocal(typeof (bool));

            var returnInstruction = cilWorker.Create(OpCodes.Ret);

            //Load the current instance onto the evaluation stack
            cilWorker.Emit(OpCodes.Ldarg_0);
            //Load the field member representing the event handler
            cilWorker.Emit(OpCodes.Ldfld, _eventHander);            
            //Load a null value
            cilWorker.Emit(OpCodes.Ldnull);
            //Pushes the value of 1 onto the evaluation stack if the values are equal, otherwise 0
            cilWorker.Emit(OpCodes.Ceq);
            //Pops the result of the equals operation off the evaluation stack and stores it in the local varible at index 0 
            cilWorker.Emit(OpCodes.Stloc_0);
            //Load the value at local variable index 0 onto the evaluation stack
            cilWorker.Emit(OpCodes.Ldloc_0);
            //Transfers controls to the return instruction of the value is not 0  
            cilWorker.Emit(OpCodes.Brtrue_S, returnInstruction);
            //Load the current instance onto the evaluation stack
            cilWorker.Emit(OpCodes.Ldarg_0);
            
            //Load the field member representing the event handler
            cilWorker.Emit(OpCodes.Ldfld, _eventHander);

            //Load the current instance onto the evaluation stack (object sender)
            cilWorker.Emit(OpCodes.Ldarg_0);
            
            //Load the first parameter onto the evaluation stack (string propertyName)
            cilWorker.Emit(OpCodes.Ldarg_1);

            //Create a new instance of the PropertyChangedEventArgs class
            cilWorker.Emit(OpCodes.Newobj, constructor);

            //Invoke the event handler
            cilWorker.Emit(OpCodes.Callvirt, invokeMethod);

            //Return from the OnPropertyChanged method.
            cilWorker.Append(returnInstruction);
        }



        protected virtual void CreateEventHandler()
        {

            var constructor = _baseType.Module.ImportConstructor<NonSerializedAttribute>();

            var nonSerializedAttribute = new CustomAttribute(constructor);
            
            var fieldType = _baseType.Module.Import(typeof(PropertyChangedEventHandler));
            _eventHander = new FieldDefinition("_propertyChangedEventHandler", fieldType, FieldAttributes.Private);                                                     
            _eventHander.CustomAttributes.Add(nonSerializedAttribute);
            

            _baseType.Fields.Add(_eventHander);           
        }


        protected virtual void PatchProperties()
        {
            _baseType.Properties.Cast<PropertyDefinition>().Where(p => p.SetMethod  != null).Select(p => p.SetMethod)
                .ForEach(PatchPropertySetter);
        }

        protected virtual void PatchPropertySetter(MethodDefinition setterMethod)
        {
            var propertyName = setterMethod.Name.Substring(4);
            
            var cilWorker = setterMethod.GetILGenerator();

            //Find the instruction that represents OpCodes.Ret
            Instruction returnInstruction = setterMethod.Body.Instructions[setterMethod.Body.Instructions.Count - 1];
            
            //Temporarily remove the return instruction  
            cilWorker.Remove(returnInstruction);

            //Load the instance onto the evaluation stack
            cilWorker.Emit(OpCodes.Ldarg_0);

            //Load the property name onto the evaluation stack
            cilWorker.Emit(OpCodes.Ldstr, propertyName);

            //Call the OnPropertyChanged method
            cilWorker.Emit(OpCodes.Call, _onPropertyChangedMethod);

            //Re-insert the return instruction.
            cilWorker.Append(returnInstruction);
        }



        public void Initialize(IServiceRequestResult source)
        {
            CreateEventHandler();
            EmitOnPropertyChanged();
            PatchProperties();
        }
    }
}
